const jspack = require("jspack").jspack
const Mavlink20 = require("./mavlink20")
const Mavlink20Header = require("./mavlink20_header")
const Mavlink20BadData = require("./mavlink20_bad_data")
const Mavlink20Map = require("./mavlink20_map")

// Buffer web polyfill
class Buffer {
  static concat(uint8ArrayList) {
    return Uint8Array.from(uint8ArrayList.reduce((total, item) => [...total, ...item], []))
  }
  static alloc(length) {
    return new Uint8Array(length).fill(0)
  }
  static from(data) {
    return Uint8Array.from(data)
  }
}

class MAVLink20Processor extends Mavlink20 {
  logger;
  seq = 0;
  buf = [];
  bufInError = [];
  srcSystem = 0;
  srcComponent = 0;

  have_prefix_error = false;
  protocol_marker = 253;
  expected_length = Mavlink20.HEADER_LEN;
  little_endian = true;

  crc_extra = true;
  sort_fields = true;
  total_packets_sent = 0;
  total_bytes_sent = 0;
  total_packets_received = 0;
  total_bytes_received = 0;
  total_receive_errors = 0;
  startup_time = Date.now();

  constructor() {
    super()
  }

  log(level, message) {
    console.log(level, message);
  }

  pushBuffer(data) {
    if (data) {
      this.buf = Buffer.concat([this.buf, data]);
      this.total_bytes_received += data.length;
    }
  }

  parsePrefix() {

    // Test for a message prefix.
    if (this.buf.length >= 1 && this.buf[0] != this.protocol_marker) {

      // Strip the offending initial byte and throw an error.
      var badPrefix = this.buf[0];
      this.bufInError = this.buf.slice(0, 1);
      this.buf = this.buf.slice(1);
      this.expected_length = Mavlink20.HEADER_LEN;

      // TODO: enable subsequent prefix error suppression if robust_parsing is implemented
      //if(!this.have_prefix_error) {
      //    this.have_prefix_error = true;
      throw new Error("Bad prefix (" + badPrefix + ")");
      //}

    }
    //else if( this.buf.length >= 1 && this.buf[0] == this.protocol_marker ) {
    //    this.have_prefix_error = false;
    //}

  }

  parseLength() {

    if (this.buf.length >= 2) {
      var unpacked = jspack.Unpack('BB', this.buf.slice(0, 2));
      this.expected_length = unpacked[1] + Mavlink20.HEADER_LEN + 2 // length of message + header + CRC
    }

  }

  parsePayload() {

    var m = null;

    // If we have enough bytes to try and read it, read it.
    if (this.expected_length >= 8 && this.buf.length >= this.expected_length) {

      // Slice off the expected packet length, reset expectation to be to find a header.
      var mbuf = this.buf.slice(0, this.expected_length);
      // TODO: slicing off the buffer should depend on the error produced by the decode() function
      // - if a message we find a well formed message, cut-off the expected_length
      // - if the message is not well formed (correct prefix by accident), cut-off 1 char only
      this.buf = this.buf.slice(this.expected_length);
      this.expected_length = 6;

      // w.info("Attempting to parse packet, message candidate buffer is ["+mbuf.toByteArray()+"]");

      try {
        m = this.decode(mbuf);
        this.total_packets_received += 1;
      } catch (e) {
        // Set buffer in question and re-throw to generic error handling
        this.bufInError = mbuf;
        throw e;
      }
    }

    return m;

  }

  parseChar(c) {

    var m = null;

    try {

      this.pushBuffer(c);
      this.parsePrefix();
      this.parseLength();
      m = this.parsePayload();

    } catch (e) {

      this.log('error', e.message);
      this.total_receive_errors += 1;
      m = new Mavlink20BadData(this.bufInError, e.message);
      this.bufInError = new Buffer.from([]);

    }

    // if (null != m) {
    //     this.emit(m.name, m);
    //     this.emit('message', m);
    //     this.log
    // }

    return m;

  }


  parseBuffer = function (s) {

    // Get a message, if one is available in the stream.
    var m = this.parseChar(s);

    // No messages available, bail.
    if (null === m) {
      return null;
    }

    // While more valid messages can be read from the existing buffer, add
    // them to the array of new messages and return them.
    var ret = [m];
    while (true) {
      m = this.parseChar();
      if (null === m) {
        // No more messages left.
        return ret;
      }
      ret.push(m);
    }

  }

  decode = function (msgbuf) {

    var magic, incompat_flags, compat_flags, mlen, seq, srcSystem, srcComponent, unpacked, msgId;

    // decode the header
    try {
      unpacked = jspack.Unpack('cBBBBBBHB', msgbuf.slice(0, 10));
      magic = unpacked[0];
      mlen = unpacked[1];
      incompat_flags = unpacked[2];
      compat_flags = unpacked[3];
      seq = unpacked[4];
      srcSystem = unpacked[5];
      srcComponent = unpacked[6];
      var msgIDlow = ((unpacked[7] & 0xFF) << 8) | ((unpacked[7] >> 8) & 0xFF);
      var msgIDhigh = unpacked[8];
      msgId = msgIDlow | (msgIDhigh << 16);
    } catch (e) {
      throw new Error('Unable to unpack MAVLink header: ' + e.message);
    }

    if (magic.charCodeAt(0) != this.protocol_marker) {
      throw new Error("Invalid MAVLink prefix (" + magic.charCodeAt(0) + ")");
    }

    if (mlen != msgbuf.length - (Mavlink20.HEADER_LEN + 2)) {
      throw new Error("Invalid MAVLink message length.  Got " + (msgbuf.length - (mavlink20.HEADER_LEN + 2)) + " expected " + mlen + ", msgId=" + msgId);
    }

    if (!Mavlink20Map[msgId]) {
      throw new Error("Unknown MAVLink message ID (" + msgId + ")");
    }

    // decode the payload
    // refs: (fmt, type, order_map, crc_extra) = mavlink20.map[msgId]
    var decoder = Mavlink20Map[msgId];

    // decode the checksum
    try {
      var receivedChecksum = jspack.Unpack('<H', msgbuf.slice(msgbuf.length - 2));
    } catch (e) {
      throw new Error("Unable to unpack MAVLink CRC: " + e.message);
    }

    var messageChecksum = Mavlink20.x25Crc(msgbuf.slice(1, msgbuf.length - 2));

    // Assuming using crc_extra = True.  See the message.prototype.pack() function.
    messageChecksum = Mavlink20.x25Crc([decoder.crc_extra], messageChecksum);

    if (receivedChecksum != messageChecksum) {
      throw new Error('invalid MAVLink CRC in msgID ' + msgId + ', got 0x' + receivedChecksum + ' checksum, calculated payload checkum as 0x' + messageChecksum);
    }

    var paylen = jspack.CalcLength(decoder.format);
    var payload = msgbuf.slice(Mavlink20.HEADER_LEN, msgbuf.length - 2);

    //put any truncated 0's back in
    if (paylen > payload.length) {
      payload = Buffer.concat([payload, Buffer.alloc(paylen - payload.length)]);
    }
    // Decode the payload and reorder the fields to match the order map.
    try {
      var t = jspack.Unpack(decoder.format, payload);
    } catch (e) {
      throw new Error('Unable to unpack MAVLink payload type=' + decoder.type + ' format=' + decoder.format + ' payloadLength=' + payload + ': ' + e.message);
    }

    // Need to check if the message contains arrays
    var args = {};
    const elementsInMsg = decoder.order_map.length;
    const actualElementsInMsg = JSON.parse(JSON.stringify(t)).length;

    if (elementsInMsg == actualElementsInMsg) {
      // Reorder the fields to match the order map
      // _.each(t, function (e, i, l) {
      //     args[i] = t[decoder.order_map[i]]
      // });
      t.forEach(function (item, i) {
        args[i] = t[decoder.order_map[i]]
      })
    } else {
      // This message contains arrays
      var typeIndex = 1;
      var orderIndex = 0;
      var memberIndex = 0;
      var tempArgs = {};

      // Walk through the fields 
      for (var i = 0, size = decoder.format.length - 1; i <= size; ++i) {
        var order = decoder.order_map[orderIndex];
        var currentType = decoder.format[typeIndex];

        if (isNaN(parseInt(currentType))) {
          // This field is not an array cehck the type and add it to the args
          tempArgs[orderIndex] = t[memberIndex];
          memberIndex++;
        } else {
          // This field is part of an array, need to find the length of the array
          var arraySize = ''
          var newArray = []
          while (!isNaN(decoder.format[typeIndex])) {
            arraySize = arraySize + decoder.format[typeIndex];
            typeIndex++;
          }

          // Now that we know how long the array is, create an array with the values
          for (var j = 0, size = parseInt(arraySize); j < size; ++j) {
            newArray.push(t[j + orderIndex]);
            memberIndex++;
          }

          // Add the array to the args object
          arraySize = arraySize + decoder.format[typeIndex];
          currentType = arraySize;
          tempArgs[orderIndex] = newArray;
        }
        orderIndex++;
        typeIndex++;
      }

      // Finally reorder the fields to match the order map
      // _.each(t, function (e, i, l) {
      //     args[i] = tempArgs[decoder.order_map[i]]
      // });
      t.forEach(function (item, i) {
        args[i] = tempArgs[decoder.order_map[i]]
      })
    }

    // construct the message object
    try {
      var m = new decoder.type(...(Object.keys(args).map(key => args[key])));
      // m.set.call(m, args);
    } catch (e) {
      console.error(e);
      //throw new Error('Unable to instantiate MAVLink message of type ' + decoder.type + ' : ' + e.message);
    }
    m.msgbuf = msgbuf;
    m.payload = payload
    m.crc = receivedChecksum;
    m.header = new Mavlink20Header(msgId, mlen, seq, srcSystem, srcComponent, incompat_flags, compat_flags);
    // this.log(m);
    return m;
  }



}

module.exports = MAVLink20Processor